use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Ident, ItemFn, ItemStruct, ItemImpl, ImplItem, LitStr, Expr, FnArg, Type, TypePath};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::Token;

fn route_macro_impl(method_ident: &str, path: LitStr, input: ItemFn, layer: Option<Expr>) -> TokenStream {
    let fn_name: Ident = input.sig.ident.clone();
    let route_fn_name = Ident::new(&format!("__spring_axum_route_{}", fn_name), fn_name.span());
    let method_ident = Ident::new(method_ident, fn_name.span());
    let base_route = quote! { ::spring_axum::Router::new().route(#path, ::spring_axum::#method_ident(#fn_name)) };
    let router_stmt = if let Some(layer_expr) = layer {
        quote! { #base_route.route_layer(#layer_expr) }
    } else {
        base_route
    };
    let expanded = quote! {
        #input

        #[allow(non_snake_case)]
        pub fn #route_fn_name() -> ::spring_axum::Router {
            #router_stmt
        }
    };
    expanded.into()
}

// Parser for route attributes supporting: path literal and optional `layer = <expr>`
struct RouteArgs {
    path: LitStr,
    layer: Option<Expr>,
}

impl Parse for RouteArgs {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let path: LitStr = input.parse()?;
        let mut layer: Option<Expr> = None;
        if input.peek(Token![,]) {
            let _comma: Token![,] = input.parse()?;
            let key: Ident = input.parse()?;
            if key == "layer" {
                let _eq: Token![=] = input.parse()?;
                let expr: Expr = input.parse()?;
                layer = Some(expr);
            } else {
                return Err(syn::Error::new(key.span(), "unsupported key, expected `layer`"));
            }
        }
        Ok(Self { path, layer })
    }
}

#[proc_macro_attribute]
pub fn route_get(args: TokenStream, input: TokenStream) -> TokenStream {
    let args = parse_macro_input!(args as RouteArgs);
    let input = parse_macro_input!(input as ItemFn);
    route_macro_impl("get", args.path, input, args.layer)
}

#[proc_macro_attribute]
pub fn route_post(args: TokenStream, input: TokenStream) -> TokenStream {
    let args = parse_macro_input!(args as RouteArgs);
    let input = parse_macro_input!(input as ItemFn);
    route_macro_impl("post", args.path, input, args.layer)
}

#[proc_macro_attribute]
pub fn route_put(args: TokenStream, input: TokenStream) -> TokenStream {
    let args = parse_macro_input!(args as RouteArgs);
    let input = parse_macro_input!(input as ItemFn);
    route_macro_impl("put", args.path, input, args.layer)
}

#[proc_macro_attribute]
pub fn route_delete(args: TokenStream, input: TokenStream) -> TokenStream {
    let args = parse_macro_input!(args as RouteArgs);
    let input = parse_macro_input!(input as ItemFn);
    route_macro_impl("delete", args.path, input, args.layer)
}

fn replace_type_path_to_validated(tp: TypePath, target: &str, replacement: &str) -> Option<Type> {
    // Match last segment ident (Json or Query), replace with ::spring_axum::<ValidatedJson/ValidatedQuery>
    let last = tp.path.segments.last()?.ident.to_string();
    if last == target {
        // Extract generic argument inside <T>
        if let syn::PathArguments::AngleBracketed(args) = &tp.path.segments.last()?.arguments {
            if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() {
                let new_ty: Type = syn::parse_str(&format!("::spring_axum::{}<{}>", replacement, quote!(#inner_ty))).ok()?;
                return Some(new_ty);
            }
        }
    }
    None
}

fn validate_transform(input: ItemFn, kind: &str) -> TokenStream {
    let mut func = input.clone();
    for arg in func.sig.inputs.iter_mut() {
        if let FnArg::Typed(pt) = arg {
            if let Type::Path(tp) = &*pt.ty {
                let (target, repl) = match kind {
                    "json" => ("Json", "ValidatedJson"),
                    "query" => ("Query", "ValidatedQuery"),
                    "form" => ("Form", "ValidatedForm"),
                    "json_stream" => ("Json", "ValidatedJsonStream"),
                    _ => unreachable!(),
                };
                if let Some(new_ty) = replace_type_path_to_validated(tp.clone(), target, repl) {
                    pt.ty = Box::new(new_ty);
                }
            }
        }
    }
    quote! { #func }.into()
}

#[proc_macro_attribute]
pub fn validate_json(_args: TokenStream, input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as ItemFn);
    validate_transform(input, "json")
}

#[proc_macro_attribute]
pub fn validate_query(_args: TokenStream, input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as ItemFn);
    validate_transform(input, "query")
}

#[proc_macro_attribute]
pub fn validate_form(_args: TokenStream, input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as ItemFn);
    validate_transform(input, "form")
}

#[proc_macro_attribute]
pub fn validate_json_stream(_args: TokenStream, input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as ItemFn);
    validate_transform(input, "json_stream")
}

struct IdentList(Punctuated<Ident, Token![,]>);
impl Parse for IdentList {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        Ok(Self(Punctuated::parse_terminated(input)?))
    }
}

#[proc_macro_attribute]
pub fn controller(args: TokenStream, input: TokenStream) -> TokenStream {
    let input_struct = parse_macro_input!(input as ItemStruct);
    let idents = parse_macro_input!(args as IdentList).0;

    let routes: Vec<Ident> = idents
        .iter()
        .map(|i| Ident::new(&format!("__spring_axum_route_{}", i), i.span()))
        .collect();

    let name = input_struct.ident.clone();
    let mut merge_stmts = quote! { let mut router = ::spring_axum::Router::new(); };
    for r in routes {
        merge_stmts = quote! {
            #merge_stmts
            router = router.merge(#r());
        };
    }

    let expanded = quote! {
        #input_struct

        impl ::spring_axum::Controller for #name {
            fn routes(&self) -> ::spring_axum::Router {
                #merge_stmts
                router
            }
        }

        // Auto-register this controller's routes into inventory for discovery
        inventory::submit!(::spring_axum::ControllerRouterRegistration {
            init: || -> ::spring_axum::Router {
                #merge_stmts
                router
            },
        });
    };
    expanded.into()
}

#[proc_macro_attribute]
pub fn component(_args: TokenStream, input: TokenStream) -> TokenStream {
    let input_item = parse_macro_input!(input as ItemStruct);
    let name = input_item.ident.clone();

    // Generate inventory registration requiring Default
    let expanded = quote! {
        #input_item

        inventory::submit!(::spring_axum::ComponentRegistration {
            init: |_: &::spring_axum::ApplicationContext| -> (::std::any::TypeId, Box<dyn ::std::any::Any + Send + Sync>) {
                let value: #name = ::std::default::Default::default();
                let arc: ::std::sync::Arc<#name> = ::std::sync::Arc::new(value);
                (::std::any::TypeId::of::<#name>(), Box::new(arc))
            },
        });
    };
    expanded.into()
}

#[proc_macro_attribute]
pub fn interceptor(_args: TokenStream, input: TokenStream) -> TokenStream {
    let input_item = parse_macro_input!(input as ItemStruct);
    let name = input_item.ident.clone();

    let expanded = quote! {
        #input_item

        // Auto-register interceptor application into inventory (requires Default)
        inventory::submit!(::spring_axum::InterceptorRegistration {
            apply: |router: ::spring_axum::Router| -> ::spring_axum::Router {
                router.layer(::spring_axum::InterceptorLayer::new(#name::default()))
            },
        });
    };
    expanded.into()
}

// ---------------- Transactions & Cache Macros -----------------

#[proc_macro_attribute]
pub fn transactional(_args: TokenStream, input: TokenStream) -> TokenStream {
    let mut func = parse_macro_input!(input as ItemFn);
    let body = func.block.clone();
    func.block = Box::new(syn::parse_quote!({
        ::spring_axum::transaction(|| async move { #body }).await
    }));
    quote! { #func }.into()
}

// Attribute macro marker to opt-out of transaction wrapping inside #[tx_service]
#[proc_macro_attribute]
pub fn non_tx(_args: TokenStream, input: TokenStream) -> TokenStream {
    // Acts as a marker; actual stripping happens in tx_service. Leave item unchanged.
    input
}

// Apply transactional wrapping to methods within an impl block by default.
// Methods annotated with #[non_tx] are left unchanged.
#[proc_macro_attribute]
pub fn tx_service(_args: TokenStream, input: TokenStream) -> TokenStream {
    let mut item_impl = parse_macro_input!(input as ItemImpl);

    for impl_item in item_impl.items.iter_mut() {
        if let ImplItem::Fn(method) = impl_item {
            // Detect #[non_tx] marker and strip it
            let has_non_tx = method.attrs.iter().any(|a| a.path().is_ident("non_tx"));
            if has_non_tx {
                method.attrs.retain(|a| !a.path().is_ident("non_tx"));
                continue;
            }

            // Wrap body in spring_axum::transaction(async move { ... }).await
            let body = method.block.clone();
            method.block = syn::parse_quote!({
                ::spring_axum::transaction(|| async move { #body }).await
            });

            // Ensure method is async to allow .await in body
            if method.sig.asyncness.is_none() {
                method.sig.asyncness = Some(syn::token::Async { span: method.sig.fn_token.span });
            }
        }
    }

    quote! { #item_impl }.into()
}

// Parse optional args: `ttl = <secs>`
struct CacheArgs { ttl_secs: Option<u64> }
impl Parse for CacheArgs {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        if input.is_empty() { return Ok(Self { ttl_secs: None }); }
        let key: Ident = input.parse()?;
        if key != "ttl" { return Err(syn::Error::new(key.span(), "expected ttl = <secs>")); }
        let _eq: Token![=] = input.parse()?;
        let lit: syn::LitInt = input.parse()?;
        let secs = lit.base10_parse::<u64>()?;
        Ok(Self { ttl_secs: Some(secs) })
    }
}

fn extract_app_result_inner_ty(fn_item: &ItemFn) -> Option<Type> {
    if let syn::ReturnType::Type(_, ty_box) = &fn_item.sig.output {
        if let Type::Path(tp) = &**ty_box {
            if let Some(seg) = tp.path.segments.last() {
                // Expect something like AppResult<T>
                if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
                    if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
                        return Some(inner.clone());
                    }
                }
            }
        }
    }
    None
}

#[proc_macro_attribute]
pub fn cacheable(args: TokenStream, input: TokenStream) -> TokenStream {
    let args = parse_macro_input!(args as CacheArgs);
    let mut func = parse_macro_input!(input as ItemFn);
    let fn_name = func.sig.ident.to_string();
    let inner_ty = extract_app_result_inner_ty(&func).expect("cacheable requires return type AppResult<T>");
    let body = func.block.clone();
    let ttl_expr = if let Some(secs) = args.ttl_secs { quote! { Some(::std::time::Duration::from_secs(#secs)) } } else { quote! { None } };
    // Collect args into json map
    let mut fields = Vec::<proc_macro2::TokenStream>::new();
    for arg in func.sig.inputs.iter() {
        if let FnArg::Typed(pt) = arg {
            if let syn::Pat::Ident(pident) = &*pt.pat {
                let ident = &pident.ident;
                fields.push(quote! { stringify!(#ident) : &#ident });
            }
        }
    }
    let expanded_body = quote!({
        let __args_json = ::serde_json::json!({ #(#fields),* });
        let __key = ::spring_axum::default_cache_key(#fn_name, &__args_json);
        if let Some(__cached) = ::spring_axum::cache_instance().get_typed::<#inner_ty>(&__key) {
            return Ok(__cached);
        }
        let __res: ::spring_axum::AppResult<#inner_ty> = (async move { #body }).await;
        match __res {
            Ok(__val) => {
                ::spring_axum::cache_instance().put_typed(__key, __val.clone(), #ttl_expr);
                Ok(__val)
            }
            Err(e) => Err(e),
        }
    });
    func.block = Box::new(syn::parse_quote! { #expanded_body });
    quote! { #func }.into()
}

#[proc_macro_attribute]
pub fn cache_evict(_args: TokenStream, input: TokenStream) -> TokenStream {
    let mut func = parse_macro_input!(input as ItemFn);
    let fn_name = func.sig.ident.to_string();
    let body = func.block.clone();
    let mut fields = Vec::<proc_macro2::TokenStream>::new();
    for arg in func.sig.inputs.iter() {
        if let FnArg::Typed(pt) = arg {
            if let syn::Pat::Ident(pident) = &*pt.pat {
                let ident = &pident.ident;
                fields.push(quote! { stringify!(#ident) : &#ident });
            }
        }
    }
    let expanded_body = quote!({
        let __args_json = ::serde_json::json!({ #(#fields),* });
        let __key = ::spring_axum::default_cache_key(#fn_name, &__args_json);
        ::spring_axum::cache_instance().evict(&__key);
        (async move { #body }).await
    });
    func.block = Box::new(syn::parse_quote! { #expanded_body });
    quote! { #func }.into()
}

#[proc_macro_attribute]
pub fn cache_put(args: TokenStream, input: TokenStream) -> TokenStream {
    let args = parse_macro_input!(args as CacheArgs);
    let mut func = parse_macro_input!(input as ItemFn);
    let fn_name = func.sig.ident.to_string();
    let inner_ty = extract_app_result_inner_ty(&func).expect("cache_put requires return type AppResult<T>");
    let body = func.block.clone();
    let ttl_expr = if let Some(secs) = args.ttl_secs { quote! { Some(::std::time::Duration::from_secs(#secs)) } } else { quote! { None } };
    let mut fields = Vec::<proc_macro2::TokenStream>::new();
    for arg in func.sig.inputs.iter() {
        if let FnArg::Typed(pt) = arg {
            if let syn::Pat::Ident(pident) = &*pt.pat {
                let ident = &pident.ident;
                fields.push(quote! { stringify!(#ident) : &#ident });
            }
        }
    }
    let expanded_body = quote!({
        let __args_json = ::serde_json::json!({ #(#fields),* });
        let __key = ::spring_axum::default_cache_key(#fn_name, &__args_json);
        let __res: ::spring_axum::AppResult<#inner_ty> = (async move { #body }).await;
        match __res {
            Ok(__val) => {
                ::spring_axum::cache_instance().put_typed(__key, __val.clone(), #ttl_expr);
                Ok(__val)
            }
            Err(e) => Err(e),
        }
    });
    func.block = Box::new(syn::parse_quote! { #expanded_body });
    quote! { #func }.into()
}

// ---------------- Application Event Listener Macro -----------------
struct EventTypePath(TypePath);
impl Parse for EventTypePath {
    fn parse(input: ParseStream) -> syn::Result<Self> { Ok(Self(input.parse()?)) }
}

#[proc_macro_attribute]
pub fn event_listener(args: TokenStream, input: TokenStream) -> TokenStream {
    let event_ty = parse_macro_input!(args as EventTypePath).0;
    let func = parse_macro_input!(input as ItemFn);
    let name = func.sig.ident.clone();
    let expanded = quote! {
        #func
        inventory::submit!(::spring_axum::EventListenerRegistration {
            type_id: ::std::any::TypeId::of::<#event_ty>(),
            handle: |ev: &dyn ::std::any::Any, ctx: &::spring_axum::ApplicationContext| {
                if let Some(e) = ev.downcast_ref::<#event_ty>() {
                    #name(e, ctx);
                }
            },
        });
    };
    expanded.into()
}

// ---------------- Mapper & SQL method macros ----------------

// Marker attribute for methods to be turned into SQL calls.
// This attribute itself doesn't transform; `#[mapper]` on the impl does.
#[proc_macro_attribute]
pub fn sql(_args: TokenStream, input: TokenStream) -> TokenStream {
    input
}

// `#[mapper]` on an impl block binds XML namespace to the impl type name by default,
// and rewrites `#[sql]` methods to call into MyBatis using the method name.
// Optional usage: #[mapper(namespace = "CustomNS")] to override.
#[proc_macro_attribute]
pub fn mapper(args: TokenStream, input: TokenStream) -> TokenStream {
    // Parse optional `namespace = "..."`
    #[derive(Default)]
    struct MapperArgs { namespace: Option<String> }
    impl Parse for MapperArgs {
        fn parse(input: ParseStream) -> syn::Result<Self> {
            if input.is_empty() { return Ok(MapperArgs::default()); }
            let key: Ident = input.parse()?;
            if key != "namespace" { return Err(syn::Error::new(key.span(), "expected `namespace`")); }
            let _eq: Token![=] = input.parse()?;
            let lit: LitStr = input.parse()?;
            Ok(MapperArgs { namespace: Some(lit.value()) })
        }
    }
    let parsed_args = parse_macro_input!(args as MapperArgs);
    let mut item_impl = parse_macro_input!(input as syn::ItemImpl);

    // Determine namespace: override or type ident
    let ns = if let Some(ns) = parsed_args.namespace {
        ns
    } else {
        // Extract last ident from self type path
        match &*item_impl.self_ty {
            Type::Path(tp) => tp.path.segments.last().map(|s| s.ident.to_string()).unwrap_or_default(),
            _ => String::new(),
        }
    };
    let ns_lit = LitStr::new(&ns, proc_macro2::Span::call_site());

    // Transform methods marked with #[sql]
    let mut new_items: Vec<syn::ImplItem> = Vec::new();
    for it in item_impl.items.into_iter() {
        if let syn::ImplItem::Fn(mut m) = it {
            let has_sql = m.attrs.iter().any(|a| a.path().segments.last().map(|s| s.ident == "sql").unwrap_or(false));
            if has_sql {
                // Remove the marker attribute to avoid unused warnings
                m.attrs.retain(|a| !a.path().segments.last().map(|s| s.ident == "sql").unwrap_or(false));

                // Collect parameter idents (skip receiver)
                let mut param_idents: Vec<Ident> = Vec::new();
                for arg in m.sig.inputs.iter() {
                    if let FnArg::Typed(pt) = arg {
                        if let syn::Pat::Ident(pi) = &*pt.pat {
                            param_idents.push(pi.ident.clone());
                        }
                    }
                }

                // Build JSON pairs like "username": username
                let json_pairs: Vec<proc_macro2::TokenStream> = param_idents
                    .iter()
                    .map(|id| {
                        let key = LitStr::new(&id.to_string(), id.span());
                        quote! { #key : #id }
                    })
                    .collect();

                let method_name = m.sig.ident.clone();
                let stmt_id = quote! { concat!(#ns_lit, ".", stringify!(#method_name)) };

                // Replace body with execution
                m.block = syn::parse_quote!({
                    let params = ::serde_json::json!({ #(#json_pairs),* });
                    let exec = ::spring_axum_mybatis::NoopExecutor::default();
                    ::spring_axum::mybatis_exec!(exec, #stmt_id, params);
                    Ok(())
                });

                new_items.push(syn::ImplItem::Fn(m));
            } else {
                new_items.push(syn::ImplItem::Fn(m));
            }
        } else {
            new_items.push(it);
        }
    }

    item_impl.items = new_items;
    quote! { #item_impl }.into()
}
